#Closures and nested scopes
def enclosing():
    x = 'closed over'
    def local_func():
        print(x)
    return local_func

lf = enclosing()
lf()


#closed over
lf.__closure__


#Function factories
def raise_to(exp):
    def raise_to_exp(x):
        return pow(x, exp)
    return raise_to_exp

square = raise_to(2)
square.__closure__
square(5)
square(9)
square(1234)

cube = raise_to(3)
cube(3)
cube(10)
cube(23)


#The nonlocal keyword

message = 'global'

def enclosing():
    message = 'enclosing'

    def local():
        message = 'local'

    print('enclosing message:', message)
    local() #running local() does not affect enclosing message!
    print('enclosing message:', message)

print('global message:', message)
enclosing() #running enclosing() does not affect global message!
print('global message:', message)


#Global keyword
message = 'global'

def enclosing():
    message = 'enclosing'

    def local():
        global message   #message will now refer to global
        message = 'local'

    print('enclosing message:', message)
    local() #call to local will affect global message
            #but still not affect enclosing message
    print('enclosing message:', message)

print('global message:', message)
enclosing()
print('global message:', message)


#Accessing enclosing scopes with nonlocal
message = 'global'

def enclosing():
    message = 'enclosing'
    def local():
        nonlocal message
        message = 'local'
    print('enclosing message:', message)
    local() #Call to local will now affect enclosing message
            #But will not affect the global message.
    print('enclosing message:', message)

print('global message:', message)
enclosing()
print('global message:', message)


#nonlocal references to non existant names
message = 'global'

def enclosing():
    message = 'enclosing'
        
    def local():
        nonlocal no_such_name
        message = 'local'

    print('enclosing message:', message)
    local()
    print('enclosing message:', message)

print('global message:', message)
enclosing()
print('global message:', message)


#A more practical use of nonlocal
import time
def make_timer():
    last_called = None # Never

    def elapsed():
        nonlocal last_called
        now = time.time()
        if last_called is None:
            last_called = now
            return None
        result = now - last_called
        last_called = now
        return result

    return elapsed

t = make_timer()
t()
t()
t()
t()

t1 = make_timer()
t2 = make_timer()
t1()
t1()

t2()
t2()
t2()
t2()
t2()

t1()
t1()

